<?php

namespace Fendx\Db;

use Doctrine\Common\Cache\MemcachedCache;
use Doctrine\DBAL\SQLParserUtils;
use Doctrine\ORM\Cache\DefaultCacheFactory;
use Doctrine\ORM\Cache\RegionsConfiguration;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Setup;
use Fendx\Cache;
use Fendx\Core\Context;
use Fendx\Core\RequestContext;
use Fendx\Coroutine\Coroutine;
use Fendx\Db\PDO\EntityPool;
use Fendx\Db\PDO\PDOPool;
use Fendx\Di;
use Fendx\Exception\FendxException;
use Fendx\Config;
use Fendx\Exception\SystemException;
use Fendx\Fendx;
use Fendx\Log\EagleEye;
use Fendx\Logger;
use Mall\Phpalert\Common\Log;

/**
 * Class MysqlPDO
 * @package Fendx\Db
 */
class EntityManager extends \Fendx\Fendx
{
    /**
     * @var \PDO
     */
    /* @var $_em \Doctrine\ORM\EntityManager */
    protected $_em = null;//实体
    protected $_cfg = null;//连接配置信息

    private $_lastSQL = [];

    private $_instant_name;

    private $_affrow = 0;

    private $_transaction_enabled = false;

    protected $_timeout = 3;

    protected $options = array(
        \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
        \PDO::ATTR_ORACLE_NULLS       => \PDO::NULL_NATURAL,
        \PDO::ATTR_CASE               => \PDO::CASE_LOWER,
        \PDO::ATTR_TIMEOUT            => 3,
        \PDO::ATTR_EMULATE_PREPARES   => false,
        \PDO::ATTR_STRINGIFY_FETCHES  => false,
        \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8';"
    );

    public static $in = array();

    /**
     * factory get obj
     * @param $r
     * @param string $db
     * @throws \Exception
     * @return self
     */
    public static function Factory($r, $db = '')
    {
        $dbList = Config::get('Db');
        if (EagleEye::getGrayStatus() && isset($dbList[$db . "-gray"])) {
            $db = $db . "-gray";
        }
        $key = sprintf('PDO.%s.%s',$db, $r);
        if (!Context::has($key)) {
            if (Coroutine::inCoroutine()) {
                /**
                 * @var $pool EntityPool
                 */
                $pool = Di::factory()->get(EntityPool::class, [$r, $db]);
                $instance = $pool->get();

                Coroutine::defer(function () use ($instance, $pool){
                    $pool->release($instance);
                });
            } else {
                if (!isset(self::$in[$db][$r])) {
                    self::$in[$db][$r] = new self($r, $db);
                }
                $instance = self::$in[$db][$r];
            }
            Context::set($key, $instance);
        }
        return Context::get($key);
    }

    /**
     * 初始化Mysql对象并进行连接服务器尝试
     * \Fendx\Db\MysqlPdo constructor.
     * @param $r
     * @param $db
     * @throws FendxException pdo connect or param wrong exception
     */
    public function __construct($r, $db)
    {
        //record instant name
        $this->_instant_name = $db . "_" . $r;

        $dbConfig = Config::get('Db');

        $config = $dbConfig[$db][$r];
        if (isset($config["type"])) {
            if ($config["type"] === "random") {
                $config = $config["config"][array_rand($config["config"])];
            } else {
                throw new SystemException("SQL Config Type is Wrong!" . $this->_instant_name, -23129);
            }
        }

        //add option
        if (isset($config['options'])) {
            foreach ($config['options'] as $key => $val) {
                $this->options[$key] = $val;
            }
        }

        $this->_cfg = $config;
        $this->_cfg['port'] = empty($this->_cfg['port']) ? 3306 : $this->_cfg['port'];

        //retry connect fail
        $this->connect();
    }

    public function getInstantName() {
        return $this->_instant_name;
    }


    public function appendSqlInfo(string $sql, array $params, int $startTime) {
        //debug open will record more

        if (\Fendx\Debug::isDebug()) {
            //debug Info collect
            $debugInfo = [];
            $debugInfo['mode'] = $this->_instant_name;
            $debugInfo['sql'] = $sql;
            $debugInfo['info'] = $params;
            $debugInfo['time'] = round(microtime(true) - $startTime, 4);

            if (preg_match("/^(select )/i", $sql)) {
                $explain = $this->_em->getConnection()->executeQuery('EXPLAIN ' . $sql, $params)->fetchAll();
                if (!empty($explain)) {
                    $debugInfo['explain'] = $explain;
                }
            }

            //record sql info
            \Fendx\Debug::appendSqlInfo($debugInfo);
        }
    }

    public function appendSqlInfoByQuery(Query $qb, int $startTime) {
        //debug open will record more

        if (\Fendx\Debug::isDebug()) {
            $values = $qb->getParameters()->getValues();
            $params = [];
            foreach ($values as $val) {
                $params[] = $val->getValue();
            }
            $sql = $qb->getSQL();

            //debug Info collect
            $debugInfo = [];
            $debugInfo['mode'] = $this->_instant_name;
            $debugInfo['sql'] = $sql;
            $debugInfo['info'] = $params;
            $debugInfo['time'] = round(microtime(true) - $startTime, 4);

            if (preg_match("/^(select )/i", $sql)) {
                $explain = $this->_em->getConnection()->executeQuery('EXPLAIN ' . $sql, $params)->fetchAll();
                if (!empty($explain)) {
                    $debugInfo['explain'] = $explain;
                }
            }

            //record sql info
            \Fendx\Debug::appendSqlInfo($debugInfo);
        }
    }

    /**
     * @return \Doctrine\ORM\EntityManager
     */
    public function getEntityManager() {
        return $this->_em;
    }

    /**
     * 链接数据库,失败重试链接两次
     * 仍旧失败抛异常
     * @return bool
     * @throws FendxException 链接失败
     */
    public function connect()
    {
        //retry on connection
        $retry = 4;

        while ($retry) {
            $startTime = microtime(true);

            try {

                //prepare info of error
                $eagleeye_param = array(
                    "x_name" => "mysql.connect",
                    "x_host" => $this->_cfg['host'],
                    "x_module" => "php_mysql_connect",
                    "x_instance_name" => $this->_instant_name,
                    "x_file" => __FILE__,
                    "x_line" => __LINE__,
                );

                //if connect fail will throw exception
                //$this->_db = new \PDO($this->_cfg['dsn'], $this->_cfg['user'], $this->_cfg['pwd'], $this->options);

                if (is_null($this->_em)) {
                    $isDevMode = Config::get('Fendx.debug');

                    $memcache = null;
                    if (isset($this->_cfg['cache']) && true === $this->_cfg['cache']['is_open']) {
                        $memcachedObj = Cache::factory(Cache::CACHE_TYPE_MEMCACHE, $this->_cfg['cache']['db'] ?? 'default')->getObj();
                        $memcache = new MemcachedCache();
                        $memcache->setMemcached($memcachedObj);

                        $dbName = $this->_cfg['name'];
                        $dbVersion = $this->_cfg['cache']['version'] ?? '1.0.0';
                        $memcache->setNamespace(sprintf('%s_%s', $dbName, $dbVersion));
                    }
                    $entitiesPaths = Config::get('EntitiesPaths');
                    $config = Setup::createAnnotationMetadataConfiguration($entitiesPaths, $isDevMode, null, $memcache, false);

                    // database configuration parameters
                    $conn = array(
                        'driver' => $this->_cfg['driver'],
                        'host' => $this->_cfg['host'],
                        'port' => $this->_cfg['port'],
                        'user' => $this->_cfg['user'],
                        'password' => $this->_cfg['pwd'],
                        'dbname' => $this->_cfg['name'],
                    );

                    // 二级缓存
                    if (!is_null($memcache)) {
                        $regConfig = new RegionsConfiguration();
                        $factory   = new DefaultCacheFactory($regConfig, $memcache);
                        $config->setSecondLevelCacheEnabled();
                        $config->getSecondLevelCacheConfiguration()->setCacheFactory($factory);
                    }

                    $this->_em = \Doctrine\ORM\EntityManager::create($conn, $config);

                } else {
                    $this->_em = $this->_em->create($this->_em->getConnection(), $this->_em->getConfiguration());
                    if (!$this->_em->getConnection()->ping()) {
                        Logger::alarm('db is disconnect');
                        $this->_em->getConnection()->close();
                        $this->_em->getConnection()->connect();
                    }
                }

                $eagleeye_param["x_duration"] = round(microtime(true) - $startTime, 4);

                EagleEye::baseLog($eagleeye_param);

                return true;
            } catch (\Exception $e) {
                //record error
                $eagleeye_param["x_duration"] = round(microtime(true) - $startTime, 4);
                $eagleeye_param["x_name"] = "mysql.connect.error";
                $eagleeye_param["x_code"] = $e->getCode();
                $eagleeye_param["x_msg"] = $e->getMessage();
                EagleEye::baseLog($eagleeye_param);

                $retry--;

                if (!$retry) {
                    throw new FendxException($e->getMessage(), $e->getCode(), $e);
                }
                continue;
            }
        }
        return false;
    }

    /**
     * Ping 一下服务器，测试是否通
     * 由于PDO没有这个函数，自行实现
     * @return bool
     */
    public function ping()
    {
        try {
            $isOpen = $this->_em->isOpen();
            if (!$isOpen) {
                return false;
            }
            $mode = RequestContext::get('Debug.Mode');
            if (0 !== strcmp($mode, 'fpm')) {
                // cli 模式下 3秒内只检测一次
                // 最后一次检查连接时间
                static $lastCheckConnectionTime = 0;
                $time = time();
                if ($time - 3 > $lastCheckConnectionTime) {
                    $lastCheckConnectionTime = time();
                    if (!$this->_em->getConnection()->ping()) {
                        return false;
                    }
                }
            } else if (!$this->_em->getConnection()->ping()) {
                return false;
            }

        } catch (\Throwable $e) {
            if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) {
                return false;
            }
        }
        return true;
    }

    /**
     * 检查数据库连接,是否有效，无效则重新建立
     * @throws FendxException 链接异常
     */
    public function checkConnection()
    {
        if (!$this->ping()) {
            // $this->close();
            return $this->connect();
        }
        return true;
    }

    public function setTimeout($timeout = 3)
    {
        //$this->options[\PDO::ATTR_TIMEOUT] = $timeout;
        $this->_timeout = $timeout;
    }


    /**
     * 关闭当前数据库连接
     *
     * @return bool
     **/
    public function close()
    {
        $this->_em->getConnection()->close();
        $this->_em->close();
    }

    public function __destruct()
    {
        $this->_em = null;
    }
}
